一文搞懂SpringBoot实现动态切换数据源
在我们实现多租户业务需求时,需要从不同的数据库中获取数据然后写入到当前数据库中,因此涉及到切换数据源问题。于是采用ThreadLocal+AbstractRoutingDataSource来实现dynamic-datasource-spring-boot-starter中线程数据源切换。
简介
上述提到了ThreadLocal和AbstractRoutingDataSource,我们来对其进行简单介绍下。
ThreadLocal
:想必大家必不会陌生,全称:thread local variable。主要是为解决多线程时由于并发而产生数据不一致问题。ThreadLocal为每个线程提供变量副本,确保每个线程在某一时间访问到的不是同一个对象,这样做到了隔离性,增加了内存,但大大减少了线程同步时的性能消耗,减少了线程并发控制的复杂程度。
ThreadLocal作用:在一个线程中共享,不同线程间隔离
ThreadLocal原理:ThreadLocal存入值时,会获取当前线程实例作为key,存入当前线程对象中的Map中。
AbstractRoutingDataSource
:根据用户定义的规则选择当前的数据源,
作用:在执行查询之前,设置使用的数据源,实现动态路由的数据源,在每次数据库查询操作前执行它的抽象方法determineCurrentLookupKey(),决定使用哪个数据源。
如何实现
01定义ThreadLocal
创建一个类用于实现ThreadLocal,主要是通过get,set,remove方法来获取、设置、删除当前线程对应的数据源。
public class DataSourceContextHolder {
//此类提供线程局部变量。这些变量不同于它们的正常对应关系是每个线程访问一个线程(通过get、set方法),有自己的独立初始化变量的副本。
private static final ThreadLocal<String> DATASOURCE_HOLDER = new ThreadLocal<>();
/**
* 设置数据源
* @param dataSourceName 数据源名称
*/
public static void setDataSource(String dataSourceName){
DATASOURCE_HOLDER.set(dataSourceName);
}
/**
* 获取当前线程的数据源
* @return 数据源名称
*/
public static String getDataSource(){
return DATASOURCE_HOLDER.get();
}
/**
* 删除当前数据源
*/
public static void removeDataSource(){
DATASOURCE_HOLDER.remove();
}
}
02定义AbstractRoutingDataSourcepublic class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(DataSource defaultDataSource,Map<Object, Object> targetDataSources){
super.setDefaultTargetDataSource(defaultDataSource);
super.setTargetDataSources(targetDataSources);
}
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}
上述代码中,还实现了一个动态数据源类的构造方法,主要是为了设置默认数据源,以及以Map保存的各种目标数据源。其中Map的key是设置的数据源名称,value则是对应的数据源(DataSource)。
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
master:
url: jdbc:mysql://127.0.0.1:3306/myDb1?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
url: jdbc:mysql://xxxxx:3306/myDb2?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
initial-size: 15
min-idle: 15
max-active: 500
max-wait: 5000
time-between-eviction-runs-millis: 80000
min-evictable-idle-time-millis: 200000
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: false
connection-properties: false
@Configuration
public class DateSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource masterDataSource(){
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.druid.slave")
public DataSource slaveDataSource(){
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSource createDynamicDataSource(){
Map<Object,Object> dataSourceMap = new HashMap<>();
DataSource defaultDataSource = masterDataSource();
dataSourceMap.put("master",defaultDataSource);
dataSourceMap.put("slave",slaveDataSource());
return new DynamicDataSource(defaultDataSource,dataSourceMap);
}
}
通过配置类,将配置文件中的配置的数据库信息转换成datasource,并添加到DynamicDataSource中,同时通过@Bean将DynamicDataSource注入Spring中进行管理,后期在进行动态数据源添加时,会用到。
在myDb1,myDb2两个测试库中,分别添加一张表test,里面只有一个字段account。
create table test(
account varchar(30) not null comment '账户'
)
#db1新增数据
insert into test(account) value ('db1');
#db2新增数据
insert into test(account) value ('db2');
新建方法,参数是需要查询数据的数据源名称。
@GetMapping("/test.do/{datasourceName}")
public String getTest(@PathVariable("dsName") String dsName){
DataSourceContextHolder.setDataSource(dsName);
Test test = tesMapper.selectOne(null);
DataSourceContextHolder.removeDataSource();
return test.getAccount();
}
执行结果:传递master时返回db1,传递slave时返回db2。
通过执行结果,我们看到传递不同的数据源名称,查询对应的数据库是不一样的,返回结果也不一样。
在上述代码中,我们看到DataSourceContextHolder.setDataSource(datasourceName);
来设置了当前线程需要查询的数据库,通过DataSourceContextHolder.removeDataSource();
来移除当前线程已设置的数据源。
注意:
启动程序时,小伙伴不要忘记将SpringBoot自动添加数据源进行排除哦,否则会报循环依赖问题。
@SpringBootApplication(exclude=DataSourceAutoConfiguration.class)在上述中,虽然已经实现了动态切换数据源,但是我们会发现如果涉及到多个业务进行切换数据源的话,我们就需要在每一个实现类中添加这一段代码。说到这有小伙伴应该就会想到使用注解来进行优化,接下来我们来实现一下。
定义注解
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DS {
String value() default "master";
}
实现AOP
@Aspect
@Component
@Slf4j
public class DSAspect {
@Pointcut("@annotation(com.jiashn.dynamic_datasource.dynamic.aop.DS)")
public void dynamicDataSource(){}
@Around("dynamicDataSource()")
public Object datasourceAround(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature)point.getSignature();
Method method = signature.getMethod();
DS ds = method.getAnnotation(DS.class);
if (Objects.nonNull(ds)){
DataSourceContextHolder.setDataSource(ds.value());
}
try {
return point.proceed();
} finally {
DataSourceContextHolder.removeDataSource();
}
}
}
添加测试
@GetMapping("/getDb1Data.do")
public String getDb1Data(){
Test test = testMapper.selectOne(null);
return test.getAccount();
}
@GetMapping("/getDb2Data.do")
@DS("slave")
public String getDb2Data(){
Test test = testMapper.selectOne(null);
return test.getAccount();
}
由于@DS中设置的默认值是:master,因此在调用主数据源时,可以不用进行添加。通过执行结果,我们通过@DS也进行了数据源的切换。
有时候我们的业务会要求我们从保存有其他数据源的数据库表中添加这些数据源,然后再根据不同的情况切换这些数据源。因此我们需要改造下DynamicDataSource来实现动态加载数据源。
@Data
@Accessors(chain = true)
public class DataSourceEntity {
private String url;
private String userName;
private String passWord;
private String driverClassName;
/**
* 数据库key,即保存Map中的key
*/
private String key;
}
实体中定义数据源的一般信息,同时定义一个key用于作为DynamicDataSource中Map中的key
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
private final Map<Object,Object> targetDataSourceMap;
public DynamicDataSource(DataSource defaultDataSource,Map<Object, Object> targetDataSources){
super.setDefaultTargetDataSource(defaultDataSource);
super.setTargetDataSources(targetDataSources);
this.targetDataSourceMap = targetDataSources;
}
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
/**
* 添加数据源信息
* @param dataSources 数据源实体集合
* @return 返回添加结果
*/
public void createDataSource(List<DataSourceEntity> dataSources){
try {
if (CollectionUtils.isNotEmpty(dataSources)){
for (DataSourceEntity ds : dataSources) {
//校验数据库是否可以连接
Class.forName(ds.getDriverClassName());
DriverManager.getConnection(ds.getUrl(),ds.getUserName(),ds.getPassWord());
//定义数据源
DruidDataSource dataSource = new DruidDataSource();
BeanUtils.copyProperties(ds,dataSource);
//申请连接时执行validationQuery检测连接是否有效,这里建议配置为TRUE,防止取到的连接不可用
dataSource.setTestOnBorrow(true);
//建议配置为true,不影响性能,并且保证安全性。
//申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
dataSource.setTestWhileIdle(true);
//用来检测连接是否有效的sql,要求是一个查询语句。
dataSource.setValidationQuery("select 1 ");
dataSource.init();
this.targetDataSourceMap.put(ds.getKey(),dataSource);
}
super.setTargetDataSources(this.targetDataSourceMap);
// 将TargetDataSources中的连接信息放入resolvedDataSources管理
super.afterPropertiesSet();
return Boolean.TRUE;
}
}catch (ClassNotFoundException | SQLException e) {
log.error("---程序报错---:{}", e.getMessage());
}
return Boolean.FALSE;
}
/**
* 校验数据源是否存在
* @param key 数据源保存的key
* @return 返回结果,true:存在,false:不存在
*/
public boolean existsDataSource(String key){
return Objects.nonNull(this.targetDataSourceMap.get(key));
}
}
在改造后的DynamicDataSource中,我们添加可以一个 private final Map<Object,Object> targetDataSourceMap,这个map会在添加数据源的配置文件时将创建的Map数据源信息通过DynamicDataSource构造方法进行初始赋值,即:DateSourceConfig类中的createDynamicDataSource()方法中。
同时我们在该类中添加了一个createDataSource方法,进行数据源的创建,并添加到map中,再通过super.setTargetDataSources(this.targetDataSourceMap)
;进行目标数据源的重新赋值。
添加数据源
create table test_db(
id int auto_increment primary key not null comment '主键Id',
url varchar(255) not null comment '数据库URL',
uname varchar(255) not null comment '用户名',
pass varchar(255) not null comment '密码',
driver_class varchar(255) not null comment '数据库驱动'
name varchar(255) not null comment '数据库名称'
)
-- 将之前的从库录入到数据库中,修改数据库名称
insert into test_db(url, uname, pass,driver_class, name)
value ('jdbc:mysql://127.0.0.1:3306/myDb2?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false',
'root','root','com.mysql.cj.jdbc.Driver','add_slave')
启动SpringBoot时添加数据源:
@Component
public class LoadDataSourceRunner implements CommandLineRunner {
@Resource
private DynamicDataSource dynamicDataSource;
@Resource
private TestDbMapper testDbMapper;
@Override
public void run(String... args) throws Exception {
List<TestDb> testDb= testDbMapper.selectList(null);
if (CollectionUtils.isNotEmpty(testDb)) {
List<DataSourceEntity> ds = new ArrayList<>();
for (TestDb testDb: testDb) {
DataSourceEntity sourceEntity = new DataSourceEntity();
BeanUtils.copyProperties(testDb,sourceEntity);
sourceEntity.setKey(testDb.getName());
ds.add(sourceEntity);
}
dynamicDataSource.createDataSource(ds);
}
}
}
SpringBoot启动后,已经将数据库表中的数据添加到动态数据源中,我们调用之前的测试方法,将数据源名称作为参数传入看看执行结果。
通过测试我们发现数据库表中的数据库被动态加入了数据源中,小伙伴可以愉快地随意添加数据源了。
版权申明:内容来源网络,仅供学习研究,版权归原创者所有。如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!
关注公众号,更多更有用的等你来
外卖天天领神卷